package org.jetbrains.plugins.scala.uast

import com.intellij.lang.Language
import com.intellij.openapi.project.Project
import com.intellij.platform.uast.testFramework.common.AllUastTypesKt.allUElementSubtypes
import junit.framework.TestResult
import org.jetbrains.plugins.scala.base.ScalaFileSetTestCase
import org.jetbrains.plugins.scala.extensions._
import org.jetbrains.plugins.scala.lang.psi.uast.withPossibleSourceTypesCheck
import org.jetbrains.plugins.scala.util.TestUtils
import org.jetbrains.plugins.scala.{Scala3Language, ScalaLanguage, ScalaVersion}
import org.jetbrains.uast.UElement
import org.junit.Ignore

import java.nio.file.{Files, Path, Paths}
import java.text.SimpleDateFormat
import java.util.Calendar
import scala.annotation.nowarn
import scala.jdk.CollectionConverters.IterableHasAsScala
import scala.util.Try

/**
 * Run this to generate [[org.jetbrains.plugins.scala.uast.ScalaUastSourceTypeMapping]]
 */
object GeneratePossibleSourceTypesMapping {
  private val format = new SimpleDateFormat("HH:mm:ss.SSS")

  private def log(message: String): Unit =
    println(s"[${format.format(Calendar.getInstance().getTime)}] $message")

  private val excluded: Set[String] = Set(
    "large.test", "large2.test" // they're just very large with ~10k references/definitions
  )

  val mappingOutputPath: Path =
    Paths.get(TestUtils.findCommunityRoot)
      .resolve("scala/uast/src/org/jetbrains/plugins/scala/uast/ScalaUastSourceTypeMapping.scala")

  def main(args: Array[String]): Unit = {
    try run()
    finally System.exit(0)
  }


  private def run(): Unit = {
    log("Gather mapping...")
    val mappings = gatherMapping()
    log(s"Finished. Found ${mappings.size} mappings.")

    val mappingsText = new StringBuilder

    for ((from, to) <- mappings.toSeq.sortBy(_._1)) {
      mappingsText ++= s"    classOf[$from] -> ClassSetKt.classSetOf(\n"

      for (toElem <- to.toSeq.sorted) {
        val txt = toElem match {
          case "ScFunctionDeclarationImpl" => "ScFunctionDeclarationImpl[_]"
          case "ScFunctionDefinitionImpl" => "ScFunctionDefinitionImpl[_]"
          case txt => txt
        }
        mappingsText ++= s"      classOf[$txt],\n"
      }

      mappingsText ++= s"    ),\n"
    }

    log("Print...")
    Files.writeString(
      mappingOutputPath,
      s"""package org.jetbrains.plugins.scala.uast
         |
         |import com.intellij.psi._
         |import com.intellij.psi.impl.source.tree.LeafPsiElement
         |import org.jetbrains.plugins.scala.lang.psi.impl._
         |import org.jetbrains.plugins.scala.lang.psi.impl.base._
         |import org.jetbrains.plugins.scala.lang.psi.impl.base.literals._
         |import org.jetbrains.plugins.scala.lang.psi.impl.base.patterns._
         |import org.jetbrains.plugins.scala.lang.psi.impl.base.types._
         |import org.jetbrains.plugins.scala.lang.psi.impl.expr._
         |import org.jetbrains.plugins.scala.lang.psi.impl.statements._
         |import org.jetbrains.plugins.scala.lang.psi.impl.statements.params._
         |import org.jetbrains.plugins.scala.lang.psi.impl.toplevel.imports._
         |import org.jetbrains.plugins.scala.lang.psi.impl.toplevel.templates._
         |import org.jetbrains.plugins.scala.lang.psi.impl.toplevel.typedef._
         |import org.jetbrains.plugins.scala.lang.scaladoc.psi.impl._
         |import org.jetbrains.uast._
         |import org.jetbrains.uast.expressions.UInjectionHost
         |import org.jetbrains.uast.util.{ClassSet, ClassSetKt}
         |
         |/**
         | * !!! ATTENTION !!!<br>
         | * This file is generated by org.jetbrains.plugins.scala.uast.GeneratePossibleSourceTypesMapping
         | */
         |object ScalaUastSourceTypeMapping {
         |  def canConvert(element: PsiElement, targets: Array[Class[_ <: UElement]]): Boolean = {
         |    val clazz = element.getClass
         |    targets.exists(target => possibleSourceTypes(target).contains(clazz))
         |  }
         |
         |  def possibleSourceTypes(uastType: Class[_ <: UElement]): ClassSet[PsiElement] =
         |    mapping.getOrElse(uastType, ClassSetKt.emptyClassSet())
         |
         |  private val mapping: Map[Class[_ <: UElement], ClassSet[PsiElement]] = Map(
         |$mappingsText
         |  )
         |}
         |""".stripMargin
    )
    log("Done.")
  }

  private def gatherMapping(): Map[String, Set[String]] = {
    var mapping = Map.empty[String, Set[String]]

    def add(classOfUElement: String, classOfElement: String): Unit = {
      mapping = mapping.updatedWith(classOfUElement) {
        set =>
          if (!set.exists(_.contains(classOfElement))) {
            log(s"    Found $classOfUElement -> $classOfElement")
          }
          Some(set.getOrElse(Set.empty) + classOfElement)
      }
    }

    @Ignore("for local running only")
    @nowarn("cat=deprecation")
    class GatheringTestSuite(path: String, lang: Language, extensions: String*) extends ScalaFileSetTestCase(path, extensions: _*) {

      override protected def needsSdk(): Boolean = true

      override def supportedInScalaVersion(version: ScalaVersion): Boolean =
        version == ScalaVersion.Latest.Scala_2_13

      override protected def getLanguage: Language = lang

      override protected def runTest(testName0: String, content: String, project: Project): Unit = if (!excluded(testName0)) {
        withPossibleSourceTypesCheck {
          log(s"Gathering from $testName0")
          val file = createLightFile(content, project)

          val plugin = new ScalaUastLanguagePlugin
          for (element <- file.depthFirst()) {
            val classOfElement = element.getClass.getSimpleName
            for {
              expectedUClass <- allUElementSubtypes.asScala
              uElement <- Try(plugin.convertElementWithParent[UElement](element, Array(expectedUClass))).toOption
              uClass <- allUElementSubtypes.asScala if uClass.isInstance(uElement)
            } {
              val classOfUElement = uClass.getSimpleName
              add(classOfUElement, classOfElement)
            }
          }
        }
      }
    }

    new GatheringTestSuite("/parser/data", ScalaLanguage.INSTANCE).run(new TestResult)
    new GatheringTestSuite("/../src", ScalaLanguage.INSTANCE, ".scala").run(new TestResult)
    new GatheringTestSuite("/parser/scala3Import/success", Scala3Language.INSTANCE, ".test").run(new TestResult)

    if (mapping.sizeIs < 10) {
      System.err.println(
        """#####################################################################################
          |# Inner tests were not run...
          |# Make sure you have -cp set to scalaUltimate in your run config.
          |# If that doesn't help try adding VM option -Didea.force.use.core.classloader=true
          |# and --add-opens options (see scalaUltimate run configuration)
          |#
          |# If you see an error mentioning a Native Library (e.g. you use Apple Silicon),
          |# copy -Djna.boot.library.path options from scalaUltimate run configuration.
          |#####################################################################################
          |""".stripMargin.trim
      )
      System.exit(1)
    }

    mapping
  }
}
